package ru.ifmo.ctd.year2008.notepad.server;

import ru.ifmo.ctd.year2008.notepad.Line;
import ru.ifmo.ctd.year2008.notepad.Text;
import ru.ifmo.ctd.year2008.notepad.diff.DiffSet;

import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;

public class Server implements Runnable {

	public static void main(String[] args) {
		new Thread(new Server()).start();
	}

	class Client {
		volatile Socket connection;
		volatile ServerReader reader;
		volatile ServerWriter writer;
		volatile Thread readerThread, writerThread;
		volatile Document document;
	}

	class HistoryElement {
		long time;
		DiffSet diffSet;
		public HistoryElement(long time, DiffSet diffSet) {
			this.time = time;
			this.diffSet = diffSet;
		}


	}

	class Document implements Runnable {
		String name;
		Text text;
		List<Client> clients;
		List<HistoryElement> history;

		synchronized void removeClient(Client c) {
			clients.remove(c);
			c.writer.objectsToWrite.clear();
			c.document = null;
		}

		synchronized void sendEveryoneTheText() {
			for (Client client : clients) {
				try {
					// client.writer.objectsToWrite.put("Text");
					client.writer.objectsToWrite.put(this.text);
					System.out.println("SENDING TEXT TO A CLIENT");
				} catch (InterruptedException e) {
					e.printStackTrace();
					break;
				}
			}
		}

		final BlockingQueue<DiffSet> queue;

		Document(String name) {
			this.name = name;
			this.text = new Text(new ArrayList<Line>());
			this.clients = new ArrayList<Client>();
			this.queue = new ArrayBlockingQueue<DiffSet>(100);
			this.history = new ArrayList<HistoryElement>();
		}

		@Override
		public void run() {
			while (!Thread.interrupted()) {
				try {
					DiffSet diffSet = queue.take();
					System.out.println("GOT DIFF " + diffSet);
					Text t = diffSet.apply(text);
					if (t != null) {
						System.out.println("APPLIED");
						text = t;
						history.add(new HistoryElement(System.currentTimeMillis(), diffSet));
						dumpDocument();
					}
					sendEveryoneTheText();
				} catch (InterruptedException e) {
					break;
				}
			}
		}
		
		synchronized void dumpDocument() {
			try {
				PrintWriter writer = new PrintWriter(this.name+".txt");
				for (HistoryElement element: history) {
					writer.println("Change at "+element.time);
					writer.println(element.diffSet);
				}
				writer.println("FINAL TEXT:");
				for (Line line: text.getLines()) {
					writer.println(line.getString());
				}
				writer.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	final static int PORT = 12345;

	ServerSocket serverSocket;
	List<Client> clients;

	ConcurrentMap<String, Document> documents;

	@Override
	public void run() {
		try {
			serverSocket = new ServerSocket(PORT);
		} catch (IOException e) {
			e.printStackTrace();
		}
		documents = new ConcurrentHashMap<String, Document>();
		clients = new ArrayList<Client>();
		while (!Thread.interrupted()) {
			Socket socket;
			try {
				socket = serverSocket.accept();
				// System.err.println("got user");
				addUser(socket);
				// System.err.println("added user");
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	synchronized void addUser(Socket socket) throws IOException {
		Client client = new Client();
		client.connection = socket;

		ServerWriter writer = new ServerWriter(client);
		client.writer = writer;
		ServerReader reader = new ServerReader(client);
		client.reader = reader;

		Thread writerThread = new Thread(writer);
		Thread readerThread = new Thread(reader);
		client.writerThread = writerThread;
		client.readerThread = readerThread;
		writerThread.start();
		readerThread.start();
	}

	synchronized void removeUser(Client user) {
		if (user.document != null) {
			user.document.removeClient(user);
		}
		user.readerThread.interrupt();
		user.writerThread.interrupt();
		user.writer.objectsToWrite.clear();
		try {
			user.connection.close();
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (clients.remove(user)) {
			System.err.println("removed user");
		}
	}

	class ServerReader implements Runnable {
		final String user;
		final ObjectInputStream input;
		final Client client;

		private ServerReader(Client client) throws IOException {
			this.client = client;
			this.input = new ObjectInputStream(
					client.connection.getInputStream());
			InetAddress inet = client.connection.getInetAddress();
			byte[] ip = inet.getAddress();
			StringBuilder user = new StringBuilder();
			for (int i = 0; i < ip.length; i++) {
				if (i > 0) {
					user.append('.');
				}
				user.append(ip[i] & 0xFF);
			}
			System.out.println("started server reader for user " + user);
			this.user = user.toString();
		}

		@Override
		public void run() {
			while (!Thread.interrupted()) {
				String command;
				try {
					command = (String) input.readObject();
				} catch (Exception e) {
					e.printStackTrace();
					break;
				}
				if (command.startsWith("Commit")) {
					if (client.document == null) {
						continue;
					}
					DiffSet diffSet;
					try {
						diffSet = (DiffSet) input.readObject();
					} catch (Exception e) {
						e.printStackTrace();
						break;
					}
					client.document.queue.offer(diffSet);
					continue;
				}
				if (command.startsWith("Close")) {
					if (client.document == null) {
						continue;
					}
					client.document.removeClient(client);
					continue;
				}

				if (command.startsWith("List")) {
					if (client.document != null) {
						client.document.removeClient(client);
					}
					String[] docs = documents.keySet().toArray(
							new String[documents.size()]);
					try {
						// client.writer.objectsToWrite.put("List");
						client.writer.objectsToWrite.put(docs);
					} catch (InterruptedException e) {
						e.printStackTrace();
						break;
					}
					continue;
				}
				if (command.startsWith("Open")) {
					if (client.document != null) {
						client.document.removeClient(client);
					}
					String docName = command.substring(5);
					Document document = documents.get(docName);
					if (document == null) {
						document = new Document(docName);
						documents.put(docName, document);
						new Thread(document).start();
					}
					client.document = document;
					synchronized (document) {
						document.clients.add(client);
					}
					try {
						// client.writer.objectsToWrite.put("Text");
						client.writer.objectsToWrite.put(document.text);
					} catch (InterruptedException e) {
						e.printStackTrace();
						break;
					}
					continue;
				}

				System.err.println("unknown command " + command);
				break;
			}
			removeUser(client);
		}
	}

	class ServerWriter implements Runnable {
		final ObjectOutputStream output;
		final Client client;
		final BlockingQueue<Object> objectsToWrite;

		ServerWriter(Client client) throws IOException {
			this.client = client;
			OutputStream outputStream = client.connection.getOutputStream();
			this.output = new ObjectOutputStream(outputStream);
			this.objectsToWrite = new ArrayBlockingQueue<Object>(100);
		}

		@Override
		public void run() {
			while (!Thread.interrupted()) {
				Object object;
				try {
					object = objectsToWrite.take();
				} catch (InterruptedException e) {
					break;
				}
				try {
					output.writeObject(object);
					output.flush();
				} catch (IOException e) {
					e.printStackTrace();
					break;
				}
			}
			removeUser(client);
		}

	}

}
